import hashlib
import logging
import json
from collections import ChainMap
from typing import Callable

from flask import Flask
from flask.wrappers import Request
from werkzeug import Response


logger = logging.getLogger(__name__)


class StandardMiddleware:
    """
    标准Flask推荐wsgi中间件处理方法
    app = create_app()
    app.wsgi_app = BaseMiddleware(app.wsgi_app)
    """

    def __init__(self, app: Callable):
        self._app: Callable = app

    def __call__(self, environ, start_response):
        return self._app(environ, start_response)


class BaseMiddleware:
    """拓展中间件，以装饰器的方式，支持嵌套，屏蔽上下文加载等其他细节"""

    def __init__(self, app: Flask = None, wsgi_handler: Callable = None, **kwargs):
        for key, val in kwargs.items():
            setattr(self, key, val)
        self.app: Flask = app
        self.wsgi_handler = wsgi_handler

    def __call__(self, environ: dict, start_response: Callable):
        return self.wsgi_handler(environ, start_response)


class ReplayBlocker(BaseMiddleware):
    """重复/重放请求拦截中间件"""

    # 重复/重放请求最短间隔时间(单位 ms)a
    MINIMUM_LIMIT_MILLISECOND = 900

    @classmethod
    def get_request_unique_id(cls, environ: dict, _request: Request) -> str:
        path_info = _request.path

        token = environ.get("HTTP_AUTHORIZATION", "")
        try:
            body = _request.json
        except Exception as _:
            body = {}
        request_info = dict(
            sorted(
                ChainMap(_request.values, body, _request.cookies).items(), key=lambda x: x[0]
            )
        )

        # 注: 20220907 前端框架返回默认时间戳对应实际时间不可信，使用时确认可信度
        _t = request_info.pop("_t", None)  # noqa
        _ = request_info.pop("csrftoken", None)  # noqa

        # 对路径、请求参数、可能存在的url参数，用户token合并计算md5值
        md5_obj = hashlib.md5(
            "".join(
                [path_info, json.dumps(request_info, sort_keys=True), token]
            ).encode()
        )

        # 对文件对象进行处理
        if _request.files.get("file"):
            # 确保此时指针在首位，且应该在首位
            _request.files["file"].seek(0)
            for val in iter(_request.files["file"].read, 10240000):
                # iter调用read遇到EOF不会自动停止进行手动停止
                if not val:
                    break
                md5_obj.update(val)
            # 流指针恢复首位，避免影响后续操作
            _request.files["file"].seek(0)
        return md5_obj.hexdigest()

    def check_replay_request(self, environ: dict, _request: Request):
        lock_with_ttl = """
            local md5 = KEYS[1]
            local expire_time = ARGV[1]
            local request_ttl = redis.call("TTL", md5)
            if request_ttl == -2 then
                redis.call("PSETEX", md5, expire_time, 1)
                return "pass"
            elseif request_ttl == -1 then
                redis.call("DEL", md5)
                return "pass"
            else
                return request_ttl
            end
        """

        redis_conn = self.app.extensions["redis"]
        cmd = redis_conn.register_script(lock_with_ttl)
        result = cmd(
            keys=[self.get_request_unique_id(environ, _request)],
            args=[self.MINIMUM_LIMIT_MILLISECOND],
        )

        return result

    def __call__(self, environ: dict, start_response: Callable):
        """
        提前获取请求上下文并保存到当前线程变量中，方便处理请求参数，无需二次处理BufferedReader steam
        此时未进入任何flask上下文环境，也不建议提前进入任何flask上下文

        如果读取了原始io需要在wsgi_app调用前恢复
        # request_info = get_input_stream(environ).read()
        # environ['wsgi.input'] = BufferedReader(BytesIO(request_info))
        """

        _ctx = self.app.request_context(environ)
        _request: Request = _ctx.request

        # 仅对幂等要求方法限制
        if _request.method not in ("POST", "PUT", "PATCH", "DELETE"):
            _response = self.wsgi_handler(environ, start_response)
        else:
            try:
                result = self.check_replay_request(environ, _request)
            except Exception as e:  # noqa
                # 不对异常进行任何处理
                logger.error("重放请求校验异常", exc_info=e)
                _response = self.wsgi_handler(environ, start_response)
            else:
                if isinstance(result, int):
                    build_response = Response("request limit", status=429)
                    build_response.headers.add("Retry-After", 1)
                    _response = build_response(environ, start_response)
                else:
                    _response = self.wsgi_handler(environ, start_response)

        # 该实例复用 请求结束时丢弃旧请求信息
        if self.app.current_request_cache.get():
            self.app.current_request_cache.set(None)

        return _response
